CompatWebView 是为了解决 WebView 的 JavaScriptInterface 注入漏洞
-
- This method can be used to allow JavaScript to control the host application. This is a powerful feature, but also presents a security risk for apps targeting JELLY_BEAN or earlier. Apps that target a version later than JELLY_BEAN are still vulnerable if the app runs on a device running Android earlier than 4.2. The most secure way to use this method is to target JELLY_BEAN_MR1 and to ensure the method is called only when running on Android 4.2 or later. With these older versions, JavaScript could use reflection to access an injected object's public fields. Use of this method in a WebView containing untrusted content could allow an attacker to manipulate the host application in unintended ways, executing Java code with the permissions of the host application. Use extreme care when using this method in a WebView which could contain untrusted content.
-
在 Android 的 api 小于17(android4.2)调用
addJavaScriptInterface
注入 java 对象会有安全风险,可以通过 js 注入反射调用到 Java 层的方法,造成安全隐患。漏洞验证案例 -
CompatWebView
的解决方案:在大于等于 android4.2 中延用addJavaScriptInterface
,在小于 android4.2 中采用另外的通道与 js 进行交互,同时保持 api 调用的一致性,CompatWebView
做到了对客户端开发透明,复用了原来addJavaScriptInterface
的 api,对前端开发也是透明的,前端不用写两套交互方式。
-
1、添加依赖
implementation 'com.sw.compat.webview:compat-webview:1.0.0'
-
2、用CompatWebView替换原来的WebView,在需要调用addJavaScriptInterface()的地方替换成方法compatAddJavascriptInterface()
webView.compatAddJavascriptInterface(new JInterface(), "JInterface");
-
3、如果需要自定义
WebViewClient
的话,必须继承自CompatWebViewClient
来替换原来的WebViewClient
,如果不自定义的话可以省掉此步骤webView.setWebViewClient(new CompatWebViewClient(){ });
下面验证一下 addJavaScriptInterface
漏洞,详细代码见漏洞验证案例
-
1、先定义一个JavascriptInterface
public class JInterface { @JavascriptInterface public void testJsCallJava(String msg, int i) { Toast.makeText(MyApp.application, msg + ":" + (i + 20), Toast.LENGTH_SHORT).show(); } } `
-
2、再将Interface通过addJavaScriptInterface添加到WebView
webView.addJavascriptInterface(new JInterface(), "JInterface");
-
3、然后我们看看在 Javascript 中就可以通过查找
window
属性中的JInterface
对象,然后反射执行一些攻击了,例如下面的例子通过反射 Android 中的Runtime
类在应用中执行 shell 脚本。function testInjectBug(){ var p = execute(["ls","/"]); console.log(convertStreamToString(p.getInputStream())); } function execute(cmdArgs) { for (var obj in window) { if ("getClass" in window[obj]) { console.log("find:"+obj); return window[obj].getClass().forName("java.lang.Runtime"). getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs); } } } function convertStreamToString(inputStream) { var result = ""; var i = inputStream.read(); while(i != -1) { var tmp = String.fromCharCode(i); result += tmp; i = inputStream.read(); } return result; }
在介绍 CompatWebView
原理之前,先总结一下 Javascript 与 Android 的通信方式
总的来说 JavaScript 与 Android native 通信的方式有三大类:使用案例
-
1.通过JavaScriptInterface注入java对象
-
Android端注入
webView.addJavascriptInterface(new JInterface(), "JInterface"); private static class JInterface { @JavascriptInterface public void testJsCallJava(String msg, int i) { Toast.makeText(MyApp.application, msg + ":" + (i + 20), Toast.LENGTH_SHORT).show(); } }
-
JS端调用
JInterface.testJsCallJava("hello", 666)
-
-
2.通过
WebViewClient
,实现shouldOverrideUrlLoading-
Android 端
WebViewClient
,复写shouldOverrideUrlLoading
webView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { try { url = URLDecoder.decode(url, "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } if (url.startsWith(SCHEME)) { Toast.makeText(CommunicateWebViewActivity.this, url, Toast.LENGTH_SHORT).show(); return true; } return super.shouldOverrideUrlLoading(view, url); } }
-
JS端调用
document.location = "jtscheme://hello" window.location.href = "jtscheme://hello"
-
或者通过H5标签
<a href="jtscheme://hello2?a=1&b=c">ShouldOverrideUrlLoading</a> <iframe src="jtscheme://hello2?a=1&b=c"/>
-
-
3.通过
WebChromeClient
,这种有四种方式prompt(提示框)、alert(警告框)、confirm(确认框)、console(log控制台)-
Android端实现WebChromeClient
webView.setWebChromeClient(new WebChromeClient() { @Override public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { Uri uri = Uri.parse(message); if (SCHEME.equals(uri.getScheme())) { String authority = uri.getAuthority(); Set<String> params = uri.getQueryParameterNames(); for (String s : params) { Log.i("COMPAT_WEB", s + ":" + uri.getQueryParameter(s)); } Toast.makeText(MyApp.application, "Prompt::" + authority, Toast.LENGTH_SHORT).show(); } return super.onJsPrompt(view, url, message, defaultValue, result); } @Override public boolean onJsAlert(WebView view, String url, String message, JsResult result) { Toast.makeText(MyApp.application, "Alert::" + message, Toast.LENGTH_SHORT).show(); return super.onJsAlert(view, url, message, result); } @Override public boolean onJsConfirm(WebView view, String url, String message, JsResult result) { Toast.makeText(MyApp.application, "Confirm::" + message, Toast.LENGTH_SHORT).show(); return super.onJsConfirm(view, url, message, result); } @Override public boolean onConsoleMessage(ConsoleMessage consoleMessage) { Toast.makeText(MyApp.application, "Console::" + consoleMessage.message(), Toast.LENGTH_SHORT).show(); return super.onConsoleMessage(consoleMessage); } });
-
JS端调用
console.log("say hello by console"); alert("say hello by alert"); confirm("say hello by confirm"); window.prompt("jtscheme://hello?a=1&b=hi");
-
-
总结:Javascript 想通知 Android 的 native 层,除了 JavascriptInterface 以外,一般采用 shouldOverrideUrlLoading 和 onJsPrompt 这两种方式,console、alert 和 confirm 这三个方法在 Javascript 中较常用不太适合。
Android native与JavaScript通信的方式有 两种: loadUrl() 和 evaluateJavascript()
webView.loadUrl("javascript:" + javascript);
webView.evaluateJavascript(javascript, null);
-
evaluateJavascript(String script, ValueCallback resultCallback)
-
Asynchronously evaluates JavaScript in the context of the currently displayed page.官方说明
-
建议 loadUrl() 在低于 18 的版本中使用,在大于等于 19 版本中,应该使用 evaluateJavascript() ,如下面的例子所示官方迁移说明
public void compatEvaluateJavascript(String javascript) { if (Build.VERSION.SDK_INT <= 18) { loadUrl("javascript:" + javascript); } else { evaluateJavascript(javascript, null); } }
CompatWebView
在 api17 及其以上延用了在 addJavaScriptInterface,在小于
api17 中采用另外的通道与 js 进行交互,通过 shouldOverrideUrlLoading
通道来让 js 层通知到 Android,通信流程如下:
-
1.Android 层添加注入对象时调用
compatAddJavaScriptInterface
来添加 JavaScriptInterface -
2.在
compatAddJavaScriptInterface
中会去判断 sdk 的等级,大于等于 17 走原有的通道,小于 17 会先把该对象存起来 -
3.在网页加载完的时候,即 onPageFinished 回调中去解析上个步骤存起来的对象,把该对象和所有需要注入的方法解析出来,组织成为一段注入的 js 语句,类似如下:
window.JInterface = {}; window.JInterface.testJsCallJava = function(param0,param1){ schemeEncode = encodeURIComponent("JInterface?fun=testJsCallJava¶m0="+param0+"¶m1="+param1); window.location.href ="compatscheme://"+schemeEncode; };
-
4.将上面的 js 调用 webView.loadUrl() 注入到网页中
-
5.前端在需要调用的地方跟之前一样去调用
JInterface.testJsCallJava("jsCallJava success", 20)
-
6.执行上面的 js 后 Android 端在
shouldOverrideUrlLoading
通道就会收到 scheme,从 scheme 中解析出对象和对应的方法,然后再反射调用对应的方法就完成了本次通信
-
1.CompatWebView 通过
compatAddJavascriptInterface
添加 Interface 对象,sdk 版本大于等于 17 调用原生 WebView 的addJavascriptInterface,小于
17 的会把对象和对象名存在HashMap
中public void compatAddJavascriptInterface(Object object, String name) { if (Build.VERSION.SDK_INT >= 17) { addJavascriptInterface(object, name); } else { injectHashMap.put(name, object); } }
-
2.在网页加载完毕的时候会回调
CompatWebViewClient
中的onPageFinished
方法,在方法中会判断如果sdk版本低于17会将调用CompatWebView
的onPageFinished
@Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); if (Build.VERSION.SDK_INT < 17) { if (view instanceof CompatWebView) { ((CompatWebView) view).onPageFinished(); } } }
-
3.在
onPageFinished
中会遍历第一步中存入的HashMap对象,调用injectJsInterfaceForCompat
来根据对象和对象名注入jsvoid onPageFinished() { for (String name : injectHashMap.keySet()) { Object object = injectHashMap.get(name); injectJsInterfaceForCompat(object, name); } }
-
4.
injectJsInterfaceForCompat
会根据对象实例反射出需要注入的对象以及该对象需要注入的方法,拼接出一段 Js 代码,然后调用loadUrl
将 Js 注入到 WebView 中以供前端调用private void injectJsInterfaceForCompat(Object object, String name) { Class clazz = object.getClass(); Method[] methods = clazz.getMethods(); if (methods == null) { return; } StringBuilder sb = new StringBuilder("window.").append(name).append(" = {};"); for (Method method : methods) { if (!checkMethodValid(method)) { continue; } sb.append("window.").append(name).append("."); sb.append(method.getName()).append(" = function("); Class<?>[] parameterTypes = method.getParameterTypes(); int paramSize = parameterTypes.length; List<String> paramList = new ArrayList<>(); for (int i = 0; i < paramSize; i++) { String tmp = "param" + i; sb.append(tmp); paramList.add(tmp); if (i < (paramSize - 1)) { sb.append(","); } } sb.append("){schemeEncode = encodeURIComponent(\"").append(name).append("?fun=").append(method.getName()); if (paramList.size() == 0) { sb.append("\""); } else { for (int i = 0; i < paramList.size(); i++) { sb.append("&").append(paramList.get(i)).append("=\"+").append(paramList.get(i)); if (i < (paramSize - 1)) { sb.append("+\""); } } } sb.append("); window.location.href =\"").append(scheme).append("://\"").append("+schemeEncode;};"); } compatEvaluateJavascript(sb.toString()); }
上面的 Java 代码拼接出来的 Js 串类似如下所示,目的是注入一个 JInterface 对象以及 JInterface 对象的 testJsCallJava 方法,在 testJsCallJava 方法中有一个与 java 中的 testJsCallJava 方法映射的 scheme
if (window.JInterface === undefined){ window.JInterface = {}; } if (window.JInterface.testJsCallJava === undefined){ window.JInterface.testJsCallJava = function(param0,param1){ schemeEncode = encodeURIComponent("JInterface?fun=testJsCallJava¶m0="+param0+"¶m1="+param1); window.location.href ="compatscheme://"+schemeEncode; } }
-
5.完成上面的步骤后,Javascript 端就可以像之前
addJavascriptInterface
一样的通过对象调用 java 方法了function testJsCallJava(){ JInterface.testJsCallJava("jsCallJava success", 20) }
-
6.js端调用了上面的函数后,在Android端的
shouldOverrideUrlLoading
通道就会收到scheme
,在CompatWebViewClient
中会收到回调,然后转发给CompatWebView
@Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (Build.VERSION.SDK_INT < 17) { if (view instanceof CompatWebView) { if (((CompatWebView) view).shouldOverrideUrlLoading(url)) { return true; } } } return super.shouldOverrideUrlLoading(view, url); }
-
7.在
CompatWebView
中会根据 url 解析出需要反射调用的对象以及对应的方法,然后反射执行该方法,这样就完成了 sdk 低于 17 的通信流程boolean shouldOverrideUrlLoading(String url) { try { String urlDecode = URLDecoder.decode(url, "UTF-8"); if (urlDecode.startsWith(scheme)) { JavaMethod javaMethod = decodeMethodFromUri(urlDecode); if (javaMethod == null) { return false; } return javaMethod.invoke(injectHashMap); } } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return false; } private JavaMethod decodeMethodFromUri(String url) { if (url == null) { return null; } Uri decodeUri = Uri.parse(url); String dScheme = decodeUri.getScheme(); String authority = decodeUri.getAuthority(); Set<String> params = decodeUri.getQueryParameterNames(); if (!scheme.equals(dScheme) || authority == null || !params.contains("fun")) { return null; } JavaMethod javaMethod = new JavaMethod(); javaMethod.object = authority; javaMethod.methodName = decodeUri.getQueryParameter("fun"); for (String name : params) { if ("fun".equals(name)) { continue; } javaMethod.params.put(name, decodeUri.getQueryParameter(name)); } return javaMethod; }
其他WebView漏洞
- CVE-2014-1939
- CVE-2014-7224
- CNTA-2018-0005 setAllowFileAccessFromFileURLs setAllowUniversalAccessFromFileURLs
- 解决方案是在WebView中移除注入的对象,如下所示(CompatWebView中已移除),同时,setAllowFileAccessFromFileURLs和setAllowUniversalAccessFromFileURLs要设置为false或者加白名单。 ```
removeJavascriptInterface("searchBoxJavaBridge_");
removeJavascriptInterface("accessibility");
removeJavascriptInterface("accessibilityTraversal");